import sys
import math
import random
import threading
from PyQt6.QtWidgets import QApplication, QMainWindow, QGraphicsView, QGraphicsScene
from PyQt6.QtCore import QTimer, QRectF, QPointF, pyqtSignal, QObject
from PyQt6.QtGui import QBrush, QColor, QPen, QPolygonF
from Box2D import b2World, b2PolygonShape, b2CircleShape, b2_staticBody, b2_dynamicBody

PPM = 20.0
WIDTH, HEIGHT = 800, 600
TIME_STEP = 1.0 / 60
VEL_ITERS, POS_ITERS = 6, 2
BALL_RADIUS = 0.5

class SimData(QObject):
    updated = pyqtSignal(list)

class PhysicsThread(threading.Thread):
    def __init__(self, world, balls, signaler):
        super().__init__()
        self.world = world
        self.balls = balls
        self.signaler = signaler
        self.running = True

    def run(self):
        while self.running:
            self.world.Step(TIME_STEP, VEL_ITERS, POS_ITERS)
            self.world.ClearForces()

            positions = []
            for body in self.balls:
                for fixture in body.fixtures:
                    shape = fixture.shape
                    if isinstance(shape, b2CircleShape):
                        pos = (float(body.position.x), float(body.position.y))
                        positions.append(('circle', pos, shape.radius))

                    elif isinstance(shape, b2PolygonShape):
                        world_pts = [body.transform * v for v in shape.vertices]
                        positions.append(('polygon', world_pts))
            self.signaler.updated.emit(positions)
            threading.Event().wait(TIME_STEP)

    def stop(self):
        self.running = False

class Simulation(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("PyQt6 + Box2D 多线程仿真")
        self.setGeometry(100, 100, WIDTH, HEIGHT)

        self.view = QGraphicsView(self)
        self.view.setGeometry(0, 0, WIDTH, HEIGHT)
        self.scene = QGraphicsScene(0, 0, WIDTH, HEIGHT)
        self.view.setScene(self.scene)

        self.cx, self.cy = WIDTH / 2 / PPM, HEIGHT / 2 / PPM
        self.radius = min(WIDTH, HEIGHT) * 0.4 / PPM
        self.vertices = self.create_octagon()

        self.world = b2World(gravity=(0, 0), doSleep=True)
        self.create_walls()

        self.balls = []
        self.create_balls()

        self.signaler = SimData()
        self.signaler.updated.connect(self.update_scene)

        self.sim_thread = PhysicsThread(self.world, self.balls, self.signaler)
        self.sim_thread.start()

    def create_octagon(self):
        return [
            (self.cx + self.radius * math.cos(2 * math.pi / 8 * i - math.pi / 8),
             self.cy + self.radius * math.sin(2 * math.pi / 8 * i - math.pi / 8))
            for i in range(8)
        ]

    def create_walls(self):
        for i in range(8):
            p1 = self.vertices[i]
            p2 = self.vertices[(i + 1) % 8]
            dx, dy = p2[0] - p1[0], p2[1] - p1[1]
            length = math.hypot(dx, dy)
            angle = math.atan2(dy, dx)
            cx, cy = (p1[0] + p2[0]) / 2, (p1[1] + p2[1]) / 2
            self.world.CreateStaticBody(
                position=(cx, cy),
                angle=angle,
                shapes=b2PolygonShape(box=(length / 2, 0.1))
            )

    def point_in_polygon(self, x, y, poly):
        num, j = len(poly), len(poly) - 1
        inside = False
        for i in range(num):
            xi, yi = poly[i]
            xj, yj = poly[j]
            if ((yi > y) != (yj > y)) and (x < (xj - xi) * (y - yi) / (yj - yi + 1e-10) + xi):
                inside = not inside
            j = i
        return inside

    def create_circle_ball(self, x, y):
        body = self.world.CreateDynamicBody(position=(x, y))
        body.CreateCircleFixture(radius=BALL_RADIUS, density=1.0, friction=0.0, restitution=1.0)
        angle = random.uniform(0, 2 * math.pi)
        speed = 5
        body.linearVelocity = (math.cos(angle) * speed, math.sin(angle) * speed)
        return body

    def create_triangle_ball(self, x, y):
        verts = [(-0.5, -0.5), (0.5, -0.5), (0, 0.5)]
        body = self.world.CreateDynamicBody(position=(x, y))
        body.CreatePolygonFixture(vertices=verts, density=1.0, friction=0.0, restitution=1.0)
        angle = random.uniform(0, 2 * math.pi)
        speed = 5
        body.linearVelocity = (math.cos(angle) * speed, math.sin(angle) * speed)
        return body

    def create_balls(self):
        num_circles = 10
        num_triangles = 5
        while len(self.balls) < num_circles + num_triangles:
            x = random.uniform(self.cx - self.radius, self.cx + self.radius)
            y = random.uniform(self.cy - self.radius, self.cy + self.radius)
            if self.point_in_polygon(x, y, self.vertices):
                if len(self.balls) < num_circles:
                    self.balls.append(self.create_circle_ball(x, y))
                else:
                    self.balls.append(self.create_triangle_ball(x, y))

    def update_scene(self, object_states):
        self.scene.clear()

        # Draw boundary
        boundary = [QPointF(x * PPM, HEIGHT - y * PPM) for x, y in self.vertices]
        self.scene.addPolygon(QPolygonF(boundary), QPen(QColor("red"), 3))

        for item in object_states:
            if item[0] == 'circle':
                _, (x, y), r = item
                x, y, r = x * PPM, HEIGHT - y * PPM, r * PPM
                self.scene.addEllipse(x - r, y - r, 2 * r, 2 * r, QPen(), QBrush(QColor("green")))
            elif item[0] == 'polygon':
                pts = [QPointF(p[0] * PPM, HEIGHT - p[1] * PPM) for p in item[1]]
                self.scene.addPolygon(QPolygonF(pts), QPen(), QBrush(QColor("orange")))

    def closeEvent(self, event):
        self.sim_thread.stop()
        self.sim_thread.join()
        event.accept()

if __name__ == '__main__':
    app = QApplication(sys.argv)
    sim = Simulation()
    sim.show()
    sys.exit(app.exec())
